//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//********************************************************* 

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using Microsoft.Office.Core;
using Microsoft.TridentWordAddIn.Common;

namespace Microsoft.TridentWordAddIn.Presentation
{
    /// <summary>
    /// Wrapper class for IDatastorage object that uses CustomXMLParts from word.
    /// </summary>
    public class DataStore : IDataStore
    {
        #region Properties

        private const string ModelId = "Model";
        private const int DataLimit = 5242880;
        private IDocumentStorage documentStorage;
        private Dictionary<int, SerializableDictionary<string, DocumentFile>> docStores;

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of the <see cref="DataStore"/> class.
        /// </summary>
        /// <param name="storage">The storage.</param>
        public DataStore(IDocumentStorage storage)
        {
            this.docStores = new Dictionary<int, SerializableDictionary<string, DocumentFile>>();
            this.documentStorage = storage;
        }

        #endregion

        #region IDataStore Members

        /// <summary>
        /// Stores the data.
        /// </summary>
        /// <param name="documentKey">The document key.</param>
        /// <param name="guid">The GUID.</param>
        /// <param name="data">The data.</param>
        /// <returns></returns>
        public DocumentFile StoreData(int documentKey, string guid, Stream data)
        {
            int fragmentCount = 0;
            int leftOverFileSize = 0;
            CalculateFileChunkSize((int)data.Length, ref fragmentCount, ref leftOverFileSize);

            DocumentFile tempFile = null;
            DocumentFile documentFile = null;
            string savedLsid = null;
            string hashString = string.Empty;

            for (int i = 0; i < fragmentCount; i++)
            {
                if (i == 0)
                {
                    hashString = this.StoreChunkedData(documentKey, guid, data, ref tempFile, DataLimit);
                    documentFile = tempFile;
                    documentFile.Id = hashString;
                }
                else
                {
                    hashString = this.StoreChunkedData(documentKey, string.Empty, data, ref tempFile, DataLimit);
                    documentFile.Length += tempFile.Length;
                }

                if (fragmentCount == 0 && leftOverFileSize == 0)
                {
                    this.documentStorage.SetNextNodeCustomXmlPart(documentKey, hashString, null);
                }
                else if (savedLsid != null)
                {
                    this.documentStorage.SetNextNodeCustomXmlPart(documentKey, savedLsid, hashString);
                    savedLsid = hashString;
                }
                else
                {
                    savedLsid = hashString;
                }
            }

            if (leftOverFileSize > 0)
            {
                hashString = this.StoreChunkedData(documentKey, string.Empty, data, ref tempFile, leftOverFileSize);
                this.documentStorage.SetNextNodeCustomXmlPart(documentKey, savedLsid, hashString);
                this.documentStorage.SetNextNodeCustomXmlPart(documentKey, hashString, null);
                documentFile.Length += tempFile.Length;
            }

            return documentFile;
        }

        /// <summary>
        /// Gets the data.
        /// </summary>
        /// <param name="documentKey">The document key.</param>
        /// <param name="filePath">The file path.</param>
        /// <param name="guid">The GUID.</param>
        /// <returns>string</returns>
        public void CreateFileFromDocumentContent(int documentKey, string filePath, Guid guid)
        {

            CustomXMLPart part = this.documentStorage.GetCustomXmlPartByGuid(documentKey, guid);
            if (part != null)
            {
                using (FileStream fileStream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write))
                {
                    byte[] buffer = GetBytes(part);
                    fileStream.Write(buffer, 0, buffer.Length);
                    fileStream.Flush();
                    string nextguid = null;

                    do
                    {
                        nextguid = this.documentStorage.GetNextNodeGuid(documentKey, "{" + guid.ToString() + "}");
                        if (nextguid != null)
                        {
                            part = this.documentStorage.GetCustomXmlPartByGuid(documentKey, new Guid(nextguid));
                            buffer = GetBytes(part);
                            fileStream.Write(buffer, 0, buffer.Length);
                            fileStream.Flush();
                            guid = new Guid(nextguid);
                        }
                    } while (nextguid != null);
                }
            }
            else
            {
                throw new ApplicationException("No file stored matching guid " + guid);
            }

        }

        /// <summary>
        /// Gets the file data.
        /// </summary>
        /// <param name="documentKey">The document key.</param>
        /// <param name="guid">The GUID.</param>
        /// <returns></returns>
        public byte[] GetFileData(int documentKey, Guid guid)
        {
            CustomXMLPart part = this.documentStorage.GetCustomXmlPartByGuid(documentKey, guid);
            if (part != null)
            {
                using (MemoryStream memoryStream = new MemoryStream())
                {
                    byte[] buffer = GetBytes(part);
                    memoryStream.Write(buffer, 0, buffer.Length);
                    string nextguid = null;

                    do
                    {
                        nextguid = this.documentStorage.GetNextNodeGuid(documentKey, "{" + guid.ToString() + "}");
                        if (nextguid != null)
                        {
                            part = this.documentStorage.GetCustomXmlPartByGuid(documentKey, new Guid(nextguid));
                            buffer = GetBytes(part);
                            memoryStream.Write(buffer, 0, buffer.Length);
                            guid = new Guid(nextguid);
                        }

                    } while (nextguid != null);

                    return memoryStream.ToArray();
                }
            }
            else
            {
                throw new ApplicationException("No file stored matching guid " + guid);
            }
        }

        /// <summary>
        /// Serializes the model.
        /// </summary>
        /// <param name="documentKey">The document key.</param>
        /// <param name="model">The model.</param>
        public void SerializeModel(int documentKey, Model model)
        {
            using (MemoryStream memoryStream = new MemoryStream())
            {
                this.Serialize(documentKey, model, memoryStream, ModelId);
            }
        }

        /// <summary>
        /// Deserializes the model.
        /// </summary>
        /// <param name="documentKey">The document key.</param>
        /// <returns>The Model</returns>
        public Model DeserializeModel(int documentKey)
        {
            try
            {
                Model model = Deserialize<Model>(documentKey, ModelId);
                return model;
            }
            catch (COMException ex)
            {
                Trace.WriteLine("Exception: " + ex, TraceLevel.Error.ToString());
            }
            catch (Exception)
            {
                throw;
            }

            return null;
        }

        /// <summary>
        /// Clears the specified document key.
        /// </summary>
        /// <param name="documentKey">The document key.</param>
        public void Clear(int documentKey)
        {
            this.GetDataStoreByDocumentKey(documentKey).Clear();
            this.documentStorage.Clear(documentKey);
        }

        /// <summary>
        /// Compresses the specified ids.
        /// </summary>
        /// <param name="documentKey">The document key.</param>
        /// <param name="idList">The id list.</param>
        public void Compress(int documentKey, List<string> idList)
        {
            SerializableDictionary<string, DocumentFile> files = this.GetDataStoreByDocumentKey(documentKey);
            Trace.WriteLine("Compressing Data Store...");
            List<string> toRemove = new List<string>();
            List<string> nextLisdList = new List<string>();

            foreach (string lsid in idList)
            {
                string nextlisd = lsid;

                do
                {
                    nextlisd = this.documentStorage.GetNextNodeLsid(documentKey, nextlisd);
                    if (string.IsNullOrEmpty(nextlisd))
                    {
                        break;
                    }

                    nextLisdList.Add(nextlisd);
                } while (!nextlisd.IsNullOrEmpty());
            }

            // File Key does not exists add it to remove list
            foreach (string lsid in files.Keys)
            {
                if (!idList.Contains(lsid) && !nextLisdList.Contains(lsid))
                {
                    // remove data since it is no longer referenced
                    toRemove.Add(lsid);
                }
            }

            // delete the entry for the file and its backing xml part
            foreach (string lsid in toRemove)
            {
                Trace.WriteLine("Removing file:" + lsid);
                DocumentFile file = files[lsid];
                files.Remove(lsid);
                this.documentStorage.DeleteCustomXmlPartByGuid(file.Guid);
            }
        }

        #endregion

        #region Private Methods

        private static void CalculateFileChunkSize(int totalFileSize, ref int fragmentCount, ref int leftOverSize)
        {
            fragmentCount = totalFileSize / DataLimit;
            leftOverSize = totalFileSize % DataLimit;

            if (fragmentCount == 0 && leftOverSize > 0)
            {
                fragmentCount = 1;
                leftOverSize = 0;
            }
        }

        /// <summary>
        /// Gets the XML stream.
        /// </summary>
        /// <param name="customXMLPart">The custom XML part.</param>
        /// <returns>Stream</returns>
        private static Stream GetXmlStream(CustomXMLPart customXMLPart)
        {
            CustomXMLNode customXMLNode = customXMLPart.SelectSingleNode("//data");
            if (customXMLNode != null)
            {
                return new MemoryStream(Encoding.Default.GetBytes(customXMLNode.FirstChild.NodeValue));
            }
            else
            {
                return new MemoryStream();
            }
        }

        /// <summary>
        /// Stores the stream.
        /// </summary>
        /// <param name="part">The part.</param>
        /// <param name="dataStream">The data stream.</param>
        private static void StoreData(CustomXMLPart part, Stream dataStream)
        {
            byte[] data = new byte[dataStream.Length];
            dataStream.Position = 0;
            dataStream.Read(data, 0, data.Length);

            CustomXMLNode dataNode = part.SelectSingleNode("//data");
            if (dataNode != null)
            {
                AssignData(Convert.ToBase64String(data), dataNode, part);
            }
            else
            {
                CreateDataNode(Convert.ToBase64String(data), part);
            }
        }

        /// <summary>
        /// Assigns the data.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <param name="dataNode">The data node.</param>
        /// <param name="part">The part.</param>
        private static void AssignData(string data, CustomXMLNode dataNode, CustomXMLPart part)
        {
            IEnumerator nodes = dataNode.ChildNodes.GetEnumerator();
            nodes.MoveNext();
            if (nodes.Current != null)
            {
                CustomXMLNode contentXmlNode;
                contentXmlNode = nodes.Current as CustomXMLNode;
                if ((contentXmlNode != null) && (contentXmlNode.NodeType == MsoCustomXMLNodeType.msoCustomXMLNodeCData))
                {
                    contentXmlNode.NodeValue = data;
                }
                else
                {
                    Trace.WriteLine("Content node should be of Type 'CustomXMLNode' and mso Custom node type must be 'MsoCustomXMLNodeType.msoCustomXMLNodeCData'",
                                     TraceLevel.Error.ToString());
                }
            }
            else
            {
                AddCdataNode(data, dataNode, part);
            }
        }

        /// <summary>
        /// Creates the data node.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <param name="part">The part.</param>
        private static void CreateDataNode(string data, CustomXMLPart part)
        {
            part.DocumentElement.AppendChildNode(
                "data",
                part.DocumentElement.NamespaceURI,
                MsoCustomXMLNodeType.msoCustomXMLNodeElement,
                null);
            CustomXMLNode dataNode = part.SelectSingleNode("//data");
            AddCdataNode(data, dataNode, part);
        }

        /// <summary>
        /// Adds the cdata node.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <param name="dataNode">The data node.</param>
        /// <param name="part">The part.</param>
        private static void AddCdataNode(string data, CustomXMLNode dataNode, CustomXMLPart part)
        {
            dataNode.AppendChildNode("content", part.DocumentElement.NamespaceURI, MsoCustomXMLNodeType.msoCustomXMLNodeCData, data);
        }

        /// <summary>
        /// Gets the bytes.
        /// </summary>
        /// <param name="part">The part.</param>
        /// <returns>byte[]</returns>
        private static byte[] GetBytes(CustomXMLPart part)
        {
            string data = GetString(part);
            return Convert.FromBase64String(data);
        }

        /// <summary>
        /// Gets the string.
        /// </summary>
        /// <param name="part">The part.</param>
        /// <returns>string</returns>
        private static string GetString(CustomXMLPart part)
        {
            CustomXMLNode dataNode = part.SelectSingleNode("//data");
            if (dataNode != null)
            {
                return dataNode.Text;
            }
            else
            {
                return null;
            }
        }

        /// <summary>
        /// Stores the XML stream.
        /// </summary>
        /// <param name="customXMLPart">The custom XML part.</param>
        /// <param name="memoryStream">The memory stream.</param>
        private static void StoreXmlStream(CustomXMLPart customXMLPart, MemoryStream memoryStream)
        {
            XmlDocument xmlDocument = new XmlDocument();
            xmlDocument.LoadXml(Encoding.Default.GetString(memoryStream.ToArray()));
            CustomXMLNode customXMLNode = customXMLPart.SelectSingleNode("//data");
            if (customXMLNode != null)
            {
                customXMLNode.ReplaceChildSubtree(xmlDocument.ChildNodes[1].OuterXml, customXMLNode.FirstChild);
            }
            else
            {
                customXMLPart.DocumentElement.AppendChildNode(
                    "data",
                    customXMLPart.DocumentElement.NamespaceURI,
                    MsoCustomXMLNodeType.msoCustomXMLNodeElement,
                    string.Empty);
                customXMLNode = customXMLPart.SelectSingleNode("//data");
                customXMLNode.AppendChildSubtree(xmlDocument.ChildNodes[1].OuterXml);
            }

            xmlDocument = null;
            customXMLNode = null;
        }

        /// <summary>
        /// Serializes the specified document key.
        /// </summary>
        /// <param name="documentKey">The document key.</param>
        /// <param name="obj">The obj.</param>
        /// <param name="memoryStream">The memory stream.</param>
        /// <param name="partId">The part id.</param>
        private void Serialize(int documentKey, object obj, MemoryStream memoryStream, string partId)
        {
            try
            {
                Type type = obj.GetType();
                XmlSerializer xmlSerializer = new XmlSerializer(type);
                xmlSerializer.Serialize(memoryStream, obj);
                CustomXMLPart customXMLPart = this.documentStorage.GetCustomXmlPartById(documentKey, partId);
                StoreXmlStream(customXMLPart, memoryStream);
            }
            catch (Exception ex)
            {
                MessageBoxHelper.ShowException(ex, null);
            }
        }

        /// <summary>
        /// Deserializes the specified document key.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="documentKey">The document key.</param>
        /// <param name="part_id">The part_id.</param>
        /// <returns>T</returns>
        private T Deserialize<T>(int documentKey, string part_id)
        {
            CustomXMLPart customXMLPart = this.documentStorage.GetCustomXmlPartById(documentKey, part_id);
            if (customXMLPart != null)
            {
                using (Stream stream = GetXmlStream(customXMLPart))
                {
                    if (stream.Length > 0)
                    {
                        XmlSerializer xmlSerializer = new XmlSerializer(typeof(T));
                        return (T)xmlSerializer.Deserialize(stream);
                    }
                }
            }

            return default(T);
        }

        /// <summary>
        /// Gets the data store by document key.
        /// </summary>
        /// <param name="documentKey">The document key.</param>
        /// <returns>SerializableDictionary</returns>
        private SerializableDictionary<string, DocumentFile> GetDataStoreByDocumentKey(int documentKey)
        {
            if (this.docStores.ContainsKey(documentKey))
            {
                return this.docStores[documentKey];
            }
            else
            {
                SerializableDictionary<string, DocumentFile> newFiles = new SerializableDictionary<string, DocumentFile>();
                this.docStores.Add(documentKey, newFiles);
                return newFiles;
            }
        }

        /// <summary>
        /// Stores the chunked data.
        /// </summary>
        /// <param name="documentKey">The document key.</param>
        /// <param name="guid">The GUID.</param>
        /// <param name="fileData">The file data.</param>
        /// <param name="tempFile">The temp file.</param>
        /// <param name="maxSize">Size of the max.</param>
        /// <returns>string</returns>
        private string StoreChunkedData(int documentKey, string guid, Stream fileData, ref DocumentFile tempFile, int maxSize)
        {
            using (Stream tempStream = StreamHelper.GetChunkData(fileData, maxSize))
            {
                string hashString = guid.IsNullOrEmpty() ? Crc32.GetHashString(tempStream) : guid;
                tempFile = this.StoreFile(documentKey, hashString, tempStream);
                return hashString;
            }
        }

        /// <summary>
        /// Stores the file.
        /// </summary>
        /// <param name="documentKey">The document key.</param>
        /// <param name="id">The id.</param>
        /// <param name="data">The data.</param>
        /// <returns></returns>
        private DocumentFile StoreFile(int documentKey, string id, Stream data)
        {
            SerializableDictionary<string, DocumentFile> allFiles = this.GetDataStoreByDocumentKey(documentKey);
            DocumentFile currentFile = new DocumentFile();

            if (allFiles.ContainsKey(id))
            {
                Trace.WriteLine(String.Format("File is already stored:  returning file with lsid {0}", id));
                currentFile = allFiles[id];
            }
            else
            {
                CustomXMLPart xmlPart = this.documentStorage.GetCustomXmlPartById(documentKey, id);
                StoreData(xmlPart, data);
                currentFile.Guid = new Guid(xmlPart.Id);
                currentFile.Length = data.Length;
                currentFile.Id = id;
                allFiles.Add(id, currentFile);

                Trace.WriteLine("Stored File with lsid: " + id);
            }

            return currentFile;
        }

        #endregion
    }
}
